﻿#include "Control2DBase.h"
#include "..\Rendering\ChunkRenderer.h"
#include "..\Rendering\OGL\GLRenderer.h"

#include <gl\glew.h>
#include <wx\msw\winundef.h>
#include <gl\GL.h>

wxMutex *Control2DBase::GetSchematicMutex()
{
	static wxMutex *_mutex = new wxMutex();
	return _mutex;
}

wxCondition *Control2DBase::GetSchematicCondition()
{
	static wxCondition *_condition = new wxCondition(*GetSchematicMutex());
	return _condition;
}

/// <summary>
/// Constructor.
/// </summary>
/// <param name="viewport">The viewport this control renders.</param>
Control2DBase::Control2DBase(const Viewport &viewport, RendererBase *renderer) :
	ControlBase(viewport, renderer),
	_multiLayer(true),
	_scale(1.0f),
	_pos(),
	_moving(false),
	rMoving(false),
	camera_x(0),
	camera_y(0),
	camera_z(0),
	_delta(0, 0)
{
}

Control2DBase::~Control2DBase()
{
}

/// <summary>
/// Render all lower layers.
/// </summary>
const bool &Control2DBase::GetMultiLayer() const { return _multiLayer; }
void Control2DBase::SetMultiLayer(const bool &value)
{
	_multiLayer = value;
	Reset();
}

/// <summary>
/// The offset of the camera.
/// </summary>
const Point2f &Control2DBase::GetOffset() const { return _offset; }
void Control2DBase::SetOffset(const Point2f &value) { _offset = value; Refresh(); }

/// <summary>
/// The scale of the camera.
/// </summary>
const float &Control2DBase::GetScale() const { return _scale; }
void Control2DBase::SetScale(const float &value) { _scale = value; Refresh(); }

/// <summary>
/// Resets all values to default.
/// </summary>
void Control2DBase::ResetValues()
{
	if (GetSchematic() == nullptr)
		SetOffset(Point2f(0, 0));
	else
		Renderer->ResetValues(true);

	Renderer->PreloadChunks(false);
		
	_timer.Control = this;
	_timer.Start(13);

	camera_x = camera_y = camera_z = 0;
	_offset.Set(180, 270);
}

/// <summary>
/// Handle the mousedown event.
/// </summary>
/// <param name="position">Position of the mouse</param>
/// <param name="button">Button pressed</param>
void Control2DBase::MouseDown(const Point2i &position, const MouseButton &button)
{
	_pos = position;

    if (button == MouseButton_Left || button == MouseButton_Middle)
		_moving = true;
    else if (button == MouseButton_Right)
		rMoving = true;
}

/// <summary>
/// Handle the mousemove event.
/// </summary>
/// <param name="position">Position of the mouse</param>
/// <param name="button">Button pressed</param>
void Control2DBase::MouseMove(const Point2i &position, const MouseButton &button)
{
	if (_moving)
	{
		auto delta = position - _pos;
		_delta += Point2f(delta) / 4.0f;

		/*if (p.Y > 270)
			p.Y = 270;
		else if (p.Y < 90)
			p.Y = 90;

		SetOffset(p);*/
		_pos = position;
	}
}

/// <summary>
/// Handle the mouseup event.
/// </summary>
/// <param name="position">Position of the mouse</param>
/// <param name="button">Button pressed</param>
void Control2DBase::MouseUp(const Point2i &position, const MouseButton &button)
{
	_moving = false;
    rMoving = false;
}

/// <summary>
/// Handle the mousewheel event.
/// </summary>
/// <param name="detents">Detents moved.</param>
void Control2DBase::MouseWheel(const int &detents)
{
	if (GetSchematic() == nullptr)
		return;
}

#include <gl\glew.h>

void Control2DBase::DrawChunk3D(const int &x, const int &y, const int &z, const int &px, const int &py, const int &pz)
{
	auto pos = (z * GetSchematic()->WidthInChunks) + x;
	auto tex = Renderer->GetChunkRender(pos);

	if (tex == nullptr)
		return; // wat?

	auto rx = px * GetChunkSizeInPixels();
	auto ry = py * GetChunkSizeInPixels();
	auto rz = pz * GetChunkSizeInPixels();

	_drawingRenderer->PushState();
	_drawingRenderer->Translate(rx, ry, rz);
	_drawingRenderer->PrepareRender();

	tex->Render();
	
	_drawingRenderer->PopState();
}

// Test if segment specified by points p0 and p1 intersects AABB b
int TestSegmentAABB(Point3f p0, Point3f p1, Point3f min, Point3f max)
{
    auto c = (min + max) * 0.5f; // Box center-point
    auto e = max - c; // Box halflength extents
    auto m = (p0 + p1) * 0.5f; // Segment midpoint
    auto d = p1 - m; // Segment halflength vector
    m = m - c; // Translate box and segment to origin
    // Try world coordinate axes as separating axes
	float adx = std::abs(d.X);
    if (std::abs(m.X) > e.X + adx) return 0;
    float ady = std::abs(d.Y);
    if (std::abs(m.Y) > e.Y + ady) return 0;
    float adz = std::abs(d.Z);
    if (std::abs(m.Z) > e.Z + adz) return 0;
    // Add in an epsilon term to counteract arithmetic errors when segment is
    // (near) parallel to a coordinate axis (see text for detail)
	adx += FLT_EPSILON; ady += FLT_EPSILON; adz += FLT_EPSILON;
    // Try cross products of segment direction vector with coordinate axes
    if (std::abs(m.Y * d.Z - m.Z * d.Y) > e.Y * adz + e.Z * ady) return 0;
    if (std::abs(m.Z * d.X - m.X * d.Z) > e.X * adz + e.Z * adx) return 0;
    if (std::abs(m.X * d.Y - m.Y * d.X) > e.X * ady + e.Y * adx) return 0;
    // No separating axis found; segment must be overlapping AABB
    return 1;
}

GLint unProject(GLdouble winx, GLdouble winy, GLdouble winz, const Matrix4f &modelMatrix, const Matrix4f &projMatrix, const Point4i &viewport, GLdouble *objx, GLdouble *objy, GLdouble *objz)
{
	Matrix4f finalMatrix = projMatrix;
    Point4f in;
    Point4f out;

    finalMatrix.multiply(modelMatrix);
	
	bool succeed = finalMatrix.invert();

    if (!succeed)
		return(GL_FALSE);

    in[0]=winx;
    in[1]=winy;
    in[2]=winz;
    in[3]=1.0;

    /* Map x and y from window coordinates */
    in[0] = (in[0] - viewport[0]) / viewport[2];
    in[1] = (in[1] - viewport[1]) / viewport[3];

    /* Map to range -1 to 1 */
    in[0] = in[0] * 2 - 1;
    in[1] = in[1] * 2 - 1;
    in[2] = in[2] * 2 - 1;

	out = finalMatrix.transformVec4(in);
    if (out[3] == 0.0) return(GL_FALSE);
    out[0] /= out[3];
    out[1] /= out[3];
    out[2] /= out[3];
    *objx = out[0];
    *objy = out[1];
    *objz = out[2];
    return(GL_TRUE);
}

Point3f Control2DBase::GetOGLPos(const float &x, const float &y, const float &z)
{
	GLRenderer *gl = (GLRenderer*)_drawingRenderer;
    GLfloat winX, winY, winZ;
    GLdouble posX, posY, posZ;
 
    winX = (float)x;
    winY = (float)gl->GetViewport()[3] - (float)y;
	winZ = (float)z;

	unProject(winX, winY, winZ, gl->GetModelviewMatrix(), gl->GetProjectionMatrix(), gl->GetViewport(), &posX, &posY, &posZ);

    return Point3f(posX, posY, posZ);
}

void DrawChar(PrimitiveRendererBase *primitive, RendererBase *renderer, const wchar_t &c, const int &x, const int &y)
{
	auto data = renderer->Renderers->GetFonts()->GetDataForChar(c, false);

	primitive->Draw(Face(
		Point3f(x, y, 0),
		Point3f(x + (data.first.second.GetWidth() / data.second->GetScale()), y, 0),
		Point3f(x + (data.first.second.GetWidth() / data.second->GetScale()), y + 8, 0),
		Point3f(x, y + 8, 0),
		Colorb::White,
		data.second,
		Point2i(data.first.first.X + data.first.second.GetStartCol(), data.first.first.Y),
		data.first.second.GetWidth(), 8));
}

void DrawString(PrimitiveRendererBase *primitive, RendererBase *renderer, const wxString &str, int x, int y)
{
	for (size_t i = 0; i < str.size(); ++i)
	{
		auto &ch = str[i];
		auto data = renderer->Renderers->GetFonts()->GetDataForChar(ch, false);

		if (ch <= 32)
		{
			if (ch == 32)
				x += 4;
		}
		else
		{
			DrawChar(primitive, renderer, ch, x, y);

			x += (data.first.second.GetWidth() / data.second->GetScale()) + 1;
		}
	}
}

/// <summary>
/// Handle the draw event.
/// </summary>
float sep;
void Control2DBase::Draw()
{
	Point3i blockInCamera(floorf(camera_x / 16), floorf(camera_y / 16), floorf(-camera_z / 16));

	bool found = false;
	Point3i closest;
	float closest_dist = FLT_MAX;

	if (GetSchematic() != nullptr)
	{
		_drawingRenderer->Setup3DRender();
		_drawingRenderer->PushState();

		_drawingRenderer->Rotate(_offset.Y, 1.0f, 0.0f, 0.0f);
		_drawingRenderer->Rotate(-_offset.X, 0.0f, 1.0f, 0.0f);
		_drawingRenderer->Translate(-camera_x, -camera_y, -camera_z);

		_drawingRenderer->Scale(1, 1, -1);

		auto start = GetOGLPos(GetWidth() / 2, GetHeight() / 2, 0);
		auto end = GetOGLPos(GetWidth() / 2, GetHeight() / 2, 1);

		glLineWidth(2);

		for (int z = 0; z < GetSchematic()->LengthInChunks; ++z)
			for (int x = 0; x < GetSchematic()->WidthInChunks; ++x)
				DrawChunk3D(x, 0, z, x, 0, z);
			
		for (int x = -5; x <= 5; ++x)
			for (int y = -5; y <= 5; ++y)
				for (int z = -5; z <= 5; ++z)
				{
					Point3f mins((blockInCamera.X + x) * 16, (blockInCamera.Y + y) * 16, (blockInCamera.Z + z) * 16);
					Point3f maxs = mins + Point3f(16, 16, 16);

					if (TestSegmentAABB(start, end, mins, maxs) && !GetSchematic()->GetBlock(blockInCamera.X + x, GetSchematic()->Height - (blockInCamera.Y + y), blockInCamera.Z + z, false).IsEmpty())
					{
						auto dist = (start - ((mins + maxs) / 2)).GetLength();

						if (dist < closest_dist)
						{
							found = true;
							closest_dist = dist;
							closest = Point3i(x, y, z);
						}
					}
				}

		if (found)
		{
			Point3f mins((blockInCamera.X + closest.X) * 16, (blockInCamera.Y + closest.Y) * 16, (blockInCamera.Z + closest.Z) * 16);
			Point3f maxs = mins + Point3f(16, 16, 16);

			glEnable(GL_BLEND);
			glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

			glDepthFunc(GL_LEQUAL);
			glColor4ub(0, 0, 0, 127);
			glBegin(GL_LINES);

			const float _pix = 0.04f;

			glVertex3f(mins.X - _pix, mins.Y - _pix, mins.Z - _pix);
			glVertex3f(mins.X - _pix, mins.Y - _pix, maxs.Z + _pix);
			glVertex3f(mins.X - _pix, mins.Y - _pix, maxs.Z + _pix);
			glVertex3f(maxs.X + _pix, mins.Y - _pix, maxs.Z + _pix);
			glVertex3f(maxs.X + _pix, mins.Y - _pix, maxs.Z + _pix);
			glVertex3f(maxs.X + _pix, mins.Y - _pix, mins.Z - _pix);
			glVertex3f(maxs.X + _pix, mins.Y - _pix, mins.Z - _pix);
			glVertex3f(mins.X - _pix, mins.Y - _pix, mins.Z - _pix);

			glVertex3f(mins.X - _pix, maxs.Y + _pix, mins.Z - _pix);
			glVertex3f(mins.X - _pix, maxs.Y + _pix, maxs.Z + _pix);
			glVertex3f(mins.X - _pix, maxs.Y + _pix, maxs.Z + _pix);
			glVertex3f(maxs.X + _pix, maxs.Y + _pix, maxs.Z + _pix);
			glVertex3f(maxs.X + _pix, maxs.Y + _pix, maxs.Z + _pix);
			glVertex3f(maxs.X + _pix, maxs.Y + _pix, mins.Z - _pix);
			glVertex3f(maxs.X + _pix, maxs.Y + _pix, mins.Z - _pix);
			glVertex3f(mins.X - _pix, maxs.Y + _pix, mins.Z - _pix);

			glVertex3f(mins.X - _pix, mins.Y - _pix, mins.Z - _pix);
			glVertex3f(mins.X - _pix, maxs.Y + _pix, mins.Z - _pix);
			glVertex3f(maxs.X + _pix, mins.Y - _pix, mins.Z - _pix);
			glVertex3f(maxs.X + _pix, maxs.Y + _pix, mins.Z - _pix);
			glVertex3f(maxs.X + _pix, mins.Y - _pix, maxs.Z + _pix);
			glVertex3f(maxs.X + _pix, maxs.Y + _pix, maxs.Z + _pix);
			glVertex3f(mins.X - _pix, mins.Y - _pix, maxs.Z + _pix);
			glVertex3f(mins.X - _pix, maxs.Y + _pix, maxs.Z + _pix);

			glEnd();
			glDepthFunc(GL_LESS);

			glDisable(GL_BLEND);
		}

		_drawingRenderer->PopState();
	}

	auto prim = Renderer->CreatePrimitiveRenderer();
	_drawingRenderer->Setup2DRender();
	wxString str;
	str.Printf("Chunks rendered: %i / %i | %f | Position: %.2f %.2f %.2f | Angle: %.2f %.2f", Renderer->ChunksRendered, (GetSchematic() ? GetSchematic()->TotalChunks : 0), sep, camera_x, camera_y, camera_z, _offset.X, _offset.Y);

	_drawingRenderer->PrepareRender();

	DrawString(prim, Renderer, str, 4, 4);

	if (found)
	{
		Point3i block = blockInCamera + closest;
		auto &b = GetSchematic()->GetBlock(block.X, GetSchematic()->Height - block.Y, block.Z);
		auto &name = Renderer->Renderers->GetBlockName(b.ID, b.Data);

		str.Printf("Selected: %s", name.c_str());
		DrawString(prim, Renderer, str, 4, 12);
	}

	int y = 16;
	for (size_t i = 0; i < Renderer->GetWorkers().size(); ++i)
	{
		auto worker = Renderer->GetWorkers()[i];
		wxMutexLocker locker(worker->ChunksAccessLock);
		str.Printf("Worker %i: %s... (%i%%) [%i chunks left]", i + 1, worker->Status, worker->Percent, worker->ChunksToRender.size());
		DrawString(prim, Renderer, str, 4, y += 8);
	}

	prim->Compile(false);
	prim->Render();
	delete prim;
}

Point2i Control2DBase::PointToScreen(const Point2i &p) const
{
	Point2i offs(GetWidth() / 2, GetHeight() / 2);

	switch (GetViewport())
	{
		case Viewport_Top:
		case Viewport_Bottom:
			offs += Point2i((int)(-(GetSchematic()->Width * 8) * GetScale()), (int)(-(GetSchematic()->Length * 8) * GetScale()));
			break;
		case Viewport_Front:
		case Viewport_Back:
			offs += Point2i((int)(-(GetSchematic()->Width * 8) * GetScale()), (int)(-(GetSchematic()->Height * 8) * GetScale()));
			break;
		case Viewport_Left:
		case Viewport_Right:
			offs += Point2i((int)(-(GetSchematic()->Length * 8) * GetScale()), (int)(-(GetSchematic()->Height * 8) * GetScale()));
			break;
	}

	offs += Point2i(_offset);
	return p + offs;
}

bool Control2DBase::ChunkVisible(const int &x, const int &y) const
{
	Point2i topleft(GetWidth(), GetHeight());
	Point2i bottomright(0, 0);

	auto cx = x * (int)(GetChunkSizeInPixels() * GetScale());
	auto cy = y * (int)(GetChunkSizeInPixels() * GetScale());

	topleft = PointToScreen(Point2i(cx, cy));
	bottomright = PointToScreen(Point2i(cx + (int)(GetChunkSizeInPixels() * GetScale()), cy + (int)(GetChunkSizeInPixels() * GetScale())));

	if (bottomright.X < 0 || bottomright.Y < 0 ||
		topleft.X > GetWidth() || topleft.Y > GetHeight())
		return false;

	return true;
}

void ControlTimer::Notify()
{
	watch.Pause();
	sep = 13.0f / watch.Time();

	if (sep > 4 || sep < 0)
		sep = 1.0f;

	watch.Start();

	Point3f forward, right, up;
	Point3f(Control->_offset.X, 0, Control->_offset.Y).AngleVectors(&forward, &right, &up);
	bool changed = false;

	Point3f camera(Control->camera_x, Control->camera_y, Control->camera_z);
	float move = (wxGetKeyState(WXK_SHIFT) ? 5.0f : 2.5f) * sep;

	if (wxGetKeyState(WXK_LEFT))
	{
		camera += -move * forward;
		changed = true;
	}
	else if (wxGetKeyState(WXK_RIGHT))
	{
		camera += move * forward;
		changed = true;
	}

	if (wxGetKeyState(WXK_UP))
	{
		camera += -move * up;
		changed = true;
	}
	else if (wxGetKeyState(WXK_DOWN))
	{
		camera += move * up;
		changed = true;
	}

	if (Control->_delta.X || Control->_delta.Y)
	{
		auto p = Control->GetOffset() + Control->_delta;
		
		if (p.Y > 270)
			p.Y = 270;
		else if (p.Y < 90)
			p.Y = 90;

		Control->SetOffset(p);
		Control->_delta.Set(0, 0);
	}

	if (changed)
	{
		Control->camera_x = camera.X;
		Control->camera_y = camera.Y;
		Control->camera_z = camera.Z;
	}
	
	Control->Refresh();
}
